/*
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements.  See the NOTICE file
distributed with this work for additional information
regarding copyright ownership.  The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License.  You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied.  See the License for the
specific language governing permissions and limitations
under the License.
 */
package td.server.dao;

import static com.googlecode.objectify.ObjectifyService.ofy;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import td.domain.Draft;
import td.domain.Survey;
import td.domain.Thinker;
import td.domain.Thought;
import td.domain.delta.ThoughtAdd;
import td.domain.delta.ThoughtRead;
import td.domain.delta.TitleChange;
import td.domain.delta.VersionAdd;
import td.domain.delta.VersionPublish;
import td.domain.util.LoadGroup;
import td.shared.InvalidOperationException;
import td.shared.TooManyResultsException;

import com.google.appengine.api.datastore.Cursor;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.QueryResultIterator;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.ObjectifyService;
import com.googlecode.objectify.Ref;
import com.googlecode.objectify.Result;
import com.googlecode.objectify.annotation.Embed;
import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.cmd.Loader;
import com.googlecode.objectify.cmd.Query;

/**
 * Generic DAO for use with Objectify
 * 
 * @author Artem Shamsutdinov
 * 
 * @param <T>
 *            Entity to manage.
 */
public abstract class ObjectifyDao<T> {

	static final int BAD_MODIFIERS = Modifier.FINAL | Modifier.STATIC
			| Modifier.TRANSIENT;

	static {
		ObjectifyService.register(Survey.class);
		ObjectifyService.register(Thinker.class);
		ObjectifyService.register(Thought.class);
		ObjectifyService.register(Draft.class);
		ObjectifyService.register(ThoughtAdd.class);
		ObjectifyService.register(VersionAdd.class);
		ObjectifyService.register(VersionPublish.class);
		ObjectifyService.register(TitleChange.class);
		ObjectifyService.register(ThoughtRead.class);
	}

	protected Class<T> clazz;

	@SuppressWarnings("unchecked")
	public ObjectifyDao() {
		Type genericSuperclass = getClass().getGenericSuperclass();
		// Allow this class to be safely instantiated with or without a
		// parameterized type
		if (genericSuperclass instanceof ParameterizedType) {
			clazz = (Class<T>) ((ParameterizedType) genericSuperclass)
					.getActualTypeArguments()[0];
		} else {
			throw new RuntimeException(
					"Must be initialized with a generics parameter ObjectifyDao<T>");
		}
	}

	public Result<Key<T>> put(T entity) {
		return ofy().save().entity(entity);
	}

	public Key<T> putNow(T entity) {
		return put(entity).now();
	}

	public Result<Map<Key<T>, T>> putAll(Iterable<T> entities) {
		return ofy().save().entities(entities);
	}

	public Map<Key<T>, T> putAllNow(Iterable<T> entities) {
		return putAll(entities).now();
	}

	public Result<Void> delete(T entity) {
		return ofy().delete().entity(entity);
	}

	public void deleteNow(T entity) {
		delete(entity).now();
	}

	public Result<Void> deleteKey(Key<T> entityKey) {
		return ofy().delete().key(entityKey);
	}

	public void deleteKeyNow(Key<T> entityKey) {
		deleteKey(entityKey).now();
	}

	public Result<Void> deleteAll(Iterable<T> entities) {
		return ofy().delete().entities(entities);
	}

	public void deleteAllNow(Iterable<T> entities) {
		deleteAll(entities).now();
	}

	public Result<Void> deleteKeys(Iterable<Key<T>> keys) {
		return ofy().delete().keys(keys);
	}

	public void deleteKeysNow(Iterable<Key<T>> keys) {
		deleteKeys(keys).now();
	}

	public Ref<T> getRef(Long id, Class<? extends LoadGroup>... loadGroups) {
		return load(loadGroups).type(this.clazz).id(id);
	}

	public T get(Long id, Class<? extends LoadGroup>... loadGroups) {
		return getRef(id, loadGroups).getValue();
	}

	public Map<Long, T> get(Iterable<Long> ids,
			Class<? extends LoadGroup>... loadGroups) {
		return load(loadGroups).type(this.clazz).ids(ids);
	}

	public Ref<T> getRef(Key<T> key, Class<? extends LoadGroup>... loadGroups)
			throws EntityNotFoundException {
		return load(loadGroups).key(key);
	}

	public T get(Key<T> key, Class<? extends LoadGroup>... loadGroups)
			throws EntityNotFoundException {
		return getRef(key, loadGroups).getValue();
	}

	public Map<Key<T>, T> getByKeys(Iterable<Key<T>> keys,
			Class<? extends LoadGroup>... loadGroups) {
		return load(loadGroups).keys(keys);
	}

	public List<T> listAll(Class<? extends LoadGroup>... loadGroups) {
		return load(loadGroups).type(clazz).list();
	}

	/**
	 * Convenience method to get all objects matching a single property
	 * 
	 * @param propName
	 * @param propValue
	 * @return T matching Object
	 * @throws TooManyResultsException
	 */
	public T getByProperty(String propName, Object propValue,
			Class<? extends LoadGroup>... loadGroups)
			throws TooManyResultsException {
		Query<T> q = load(loadGroups).type(clazz).filter(propName, propValue)
				.limit(2);
		Iterator<T> fetch = q.list().iterator();
		if (!fetch.hasNext()) {
			return null;
		}
		T obj = fetch.next();
		if (fetch.hasNext()) {
			throw new TooManyResultsException(q.toString()
					+ " returned too many results");
		}
		return obj;
	}

	private Loader load(Class<? extends LoadGroup>... loadGroups) {
		Loader loader = ofy().load();
		if (loadGroups != null && loadGroups.length > 0) {
			loader = loader.group(loadGroups);
		}
		return loader;
	}

	public List<T> listByProperty(String propName, Object propValue,
			Class<? extends LoadGroup>... loadGroups) {
		return load(loadGroups).type(clazz).filter(propName, propValue).list();
	}

	/**
	 * Specify properties to filter with in '"name", value' pairs and
	 * (optionally) specify <? extends LoadGroup>.class classes at the end.
	 * 
	 * @param input
	 * @return
	 * @throws InvalidOperationException
	 */
	public List<T> listByPropertiesWithLoadGroups(Object... input)
			throws InvalidOperationException {
		Query<T> q = prepareQueryWithLoadGroups(input);
		return q.list();
	}

	// TODO: Refactor to take a single object that has a
	// setFilter(String, Object) method and a addLoadGroup(Class) method
	@SuppressWarnings("rawtypes")
	private Query<T> prepareQueryWithLoadGroups(Object... input)
			throws InvalidOperationException {
		int loadersIndex = input.length;
		Loader loader = ofy().load();
		while (loadersIndex > 0) {
			loadersIndex--;
			if (input[loadersIndex] instanceof Class) {
				loader.group((Class) input[loadersIndex]);
			} else {
				loadersIndex++;
				break;
			}
		}
		if (loadersIndex < 2) {
			throw new InvalidOperationException(
					"Pust specify at least one 'property name', 'property value' pair");
		}
		if (loadersIndex % 2 != 0) {
			throw new InvalidOperationException(
					"Each property name must be followed by a property value");
		}
		Query<T> q = loader.type(clazz);
		for (int i = 0; i < loadersIndex; i = i + 2) {
			if (!(input[i] instanceof String)) {
				throw new InvalidOperationException(
						"All property names must be strings");
			}
			q = q.filter((String) input[i], input[i + 1]);
		}
		return q;
	}

	/**
	 * Specify properties to filter with in '"name", value' pairs and
	 * (optionally) specify <? extends LoadGroup>.class classes at the end.
	 * 
	 * @param cursorStore
	 * @param input
	 * @return
	 * @throws InvalidOperationException
	 */
	public ArrayList<T> listByPropertiesWithLoadGroups(
			CursorStore<T> cursorStore, Object... input)
			throws InvalidOperationException {
		if (cursorStore == null) {
			throw new InvalidOperationException("Cursor store not provided");
		}
		Query<T> q = prepareQueryWithLoadGroups(input);
		if (cursorStore.getCursor() != null) {
			q = q.startAt(getCursor(cursorStore.getCursor()));
		}

		QueryResultIterator<T> iterator = q.iterator();
		int index = 0;
		ArrayList<T> list = new ArrayList<T>();
		while (iterator.hasNext() && index < cursorStore.getPageSize()) {
			list.add(iterator.next());
			++index;
		}
		if (iterator.hasNext()) {
			cursorStore.setCursor(getCursorString(iterator));
		} else {
			cursorStore.setCursor(null);
		}

		return list;
	}

	Cursor getCursor(String cursorString) {
		return Cursor.fromWebSafeString(cursorString);
	}

	String getCursorString(QueryResultIterator<T> iterator) {
		return iterator.getCursor().toWebSafeString();
	}

	public List<Key<T>> listKeysByProperty(String propName, Object propValue) {
		return ofy().load().type(clazz).filter(propName, propValue).keys()
				.list();
	}

	public T getByExample(T exampleObj,
			Class<? extends LoadGroup>... loadGroups)
			throws TooManyResultsException {
		Query<T> q = buildQueryByExample(exampleObj, loadGroups);
		Iterator<T> fetch = q.limit(2).list().iterator();
		if (!fetch.hasNext()) {
			return null;
		}
		T obj = fetch.next();
		if (fetch.hasNext()) {
			throw new TooManyResultsException(q.toString()
					+ " returned too many results");
		}
		return obj;
	}

	public List<T> listByExample(T exampleObj,
			Class<? extends LoadGroup>... loadGroups) {
		Query<T> queryByExample = buildQueryByExample(exampleObj, loadGroups);
		return queryByExample.list();
	}

	public Key<T> getKey(Long id) {
		return Key.create(this.clazz, id);
	}

	// TODO: Is this needed at all?
//	public Key<T> key(T obj) {
//		return ObjectifyService.factory().getKey(obj);
//	}

	public List<T> listChildren(Object parent) {
		return ofy().load().type(clazz).ancestor(parent).list();
	}

	public List<Key<T>> listChildKeys(Object parent) {
		return ofy().load().type(clazz).ancestor(parent).keys().list();
	}

	protected Query<T> buildQueryByExample(T exampleObj,
			Class<? extends LoadGroup>... loadGroups) {
		Query<T> q = load(loadGroups).type(clazz);

		// Add all non-null properties to query filter
		for (Field field : clazz.getDeclaredFields()) {
			// Ignore transient, embedded, array, and collection properties
			if (field.isAnnotationPresent(Ignore.class)
					|| (field.isAnnotationPresent(Embed.class))
					|| (field.getType().isArray())
					|| (Collection.class.isAssignableFrom(field.getType()))
					|| ((field.getModifiers() & ObjectifyDao.BAD_MODIFIERS) != 0))
				continue;

			field.setAccessible(true);

			Object value;
			try {
				value = field.get(exampleObj);
			} catch (IllegalArgumentException e) {
				throw new RuntimeException(e);
			} catch (IllegalAccessException e) {
				throw new RuntimeException(e);
			}
			if (value != null) {
				q = q.filter(field.getName(), value);
			}
		}

		return q;
	}

	/*
	 * Application-specific methods to retrieve items owned by a specific user
	 */
	// public List<T> listAllForUser() {
	// Key<Thinker> userKey = new Key<Thinker>(Thinker.class, getCurrentUser()
	// .getId());
	// return listByProperty("owner", userKey);
	// }
	// public List<T> listAllForUser() {
	// Key<T> userKey = new Key<T>(clazz, getCurrentUser().getId());
	// return listByProperty("owner", userKey);
	// }

	// private Thinker getCurrentUser() {
	// return (Thinker) RequestFactoryServlet.getThreadLocalRequest()
	// .getAttribute("loggedInUser");
	// }
	// private DatastoreObject getCurrentUser() {
	// return (DatastoreObject) RequestFactoryServlet.getThreadLocalRequest()
	// .getAttribute("loggedInUser");
	// }

}
